﻿namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.FxCop.Sdk;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class TestStatementFlow
    {
        [TestMethod]
        public void Null_is_not_a_valid_method_value()
        {
            ArgumentException exception = AssertEx.Throws<ArgumentNullException>(() =>
                {
                    var _ = new StatementFlow(null);
                });

            AssertEx.That(() => exception.Message == "Value cannot be null.\r\nParameter name: method");
        }

        [TestMethod]
        public void Method_property_is_set_from_the_constructor()
        {
            // Get this method.
            var method = TypeNodeHelper.GetTypeNode(typeof(TestStatementFlow))
                .GetMethod(Identifier.For("Method_property_is_set_from_the_constructor"));
            AssertEx.That(() => method != null, "Fix the name in the line of code above this.");

            var flow = new StatementFlow(method);
            AssertEx.That(() => flow != null);
            AssertEx.That(() => object.ReferenceEquals(flow.Method, method));
        }

        [TestMethod]
        public void Nop_are_ignored()
        {
            // When the following code is compiled in debug mode, it has a bunch of Nop's in it. If we are ignoring Nop's
            // then we will only have the two statements plus the return statement. Otherwise there are three Nop's.
            const string code = @"
public void Method()
{
    Mark(1);
    Mark(2);
}";

            Method method = GetMethod(code, true);
            StatementFlow flow = new StatementFlow(method);
            AssertEx.That(() => flow.Statements.Count() == 3);
            AssertEx.That(() => flow.Statements.All(statement => statement.NodeType != NodeType.Nop));
        }

        [TestMethod]
        public void Flow_with_no_branches_works()
        {
            // Note there is a return statement automatically generated at the end.
            const string code = @"
public void Method()
{
    Mark(1);
    Mark(2);
}";

            new bool[] { true, false }
                .Run(debugMode =>
                    {
                        Method method = GetMethod(code, false);
                        StatementFlow flow = new StatementFlow(method);
                        AssertEx.That(() => flow.Statements.Count() == 3);

                        // Get the three statements.
                        var marks = GetMarks(flow);
                        ReturnNode returnStatement = flow.Statements.OfType<ReturnNode>().First();

                        // Check the statements.
                        var helper = new FlowTestHelper(flow)
                            .SetStatement(marks[1])
                            .PreviousStatements()
                            .AllPreviousStatements()
                            .FollowingStatements(marks[2])
                            .AllFollowingStatements(marks[2], returnStatement)
                            .AllFollowingPaths(new Statement[] { marks[2], returnStatement })
                            .AllPreviousPaths();

                        helper.SetStatement(marks[2])
                            .PreviousStatements(marks[1])
                            .AllPreviousStatements(marks[1])
                            .FollowingStatements(returnStatement)
                            .AllFollowingStatements(returnStatement)
                            .AllFollowingPaths(new Statement[] { returnStatement })
                            .AllPreviousPaths(new Statement[] { marks[1] });

                        helper.SetStatement(returnStatement)
                            .PreviousStatements(marks[2])
                            .AllPreviousStatements(marks[2], marks[1])
                            .FollowingStatements()
                            .AllFollowingStatements()
                            .AllFollowingPaths()
                            .AllPreviousPaths(new Statement[] { marks[2], marks[1] });
                    });
        }

        [TestMethod]
        public void Simple_release_mode_branches_are_followed()
        {
            const string code = @"
public void Method()
{
    Mark(1);
    if (System.Console.Title == """")
    {
        Mark(2);
    }
    Mark(3);
}";

            Method method = GetMethod(code, false);
            StatementFlow flow = new StatementFlow(method);
            AssertEx.That(() => flow.Statements.Count() == 5);

            // Get the statements.
            var marks = GetMarks(flow);
            Branch branchStatement = flow.Statements.OfType<Branch>().Single();
            ReturnNode returnStatement = flow.Statements.OfType<ReturnNode>().Single();

            // Check statement flow.
            var helper = new FlowTestHelper(flow)
                .SetStatement(marks[1])
                .PreviousStatements()
                .AllPreviousStatements()
                .FollowingStatements(branchStatement)
                .AllFollowingStatements(branchStatement, marks[2], marks[3], returnStatement)
                .AllFollowingPaths(
                    new Statement[] { branchStatement, marks[2], marks[3], returnStatement },
                    new Statement[] { branchStatement, marks[3], returnStatement })
                .AllPreviousPaths();

            helper.SetStatement(branchStatement)
                .PreviousStatements(marks[1])
                .AllPreviousStatements(marks[1])
                .FollowingStatements(marks[2], marks[3])
                .AllFollowingStatements(marks[2], marks[3], returnStatement)
                .AllFollowingPaths(
                    new Statement[] { marks[2], marks[3], returnStatement },
                    new Statement[] { marks[3], returnStatement })
                .AllPreviousPaths(
                    new Statement[] { marks[1] });

            helper.SetStatement(marks[2])
                .PreviousStatements(branchStatement)
                .AllPreviousStatements(marks[1], branchStatement)
                .FollowingStatements(marks[3])
                .AllFollowingStatements(marks[3], returnStatement)
                .AllFollowingPaths(
                    new Statement[] { marks[3], returnStatement })
                .AllPreviousPaths(
                    new Statement[] { branchStatement, marks[1] });

            helper.SetStatement(marks[3])
                .PreviousStatements(marks[2], branchStatement)
                .AllPreviousStatements(marks[1], branchStatement, marks[2])
                .FollowingStatements(returnStatement)
                .AllFollowingStatements(returnStatement)
                .AllFollowingPaths(
                    new Statement[] { returnStatement })
                .AllPreviousPaths(
                    new Statement[] { marks[2], branchStatement, marks[1] },
                    new Statement[] { branchStatement, marks[1] });

            helper.SetStatement(returnStatement)
                .PreviousStatements(marks[3])
                .AllPreviousStatements(marks[1], branchStatement, marks[2], marks[3])
                .FollowingStatements()
                .AllFollowingStatements()
                .AllFollowingPaths()
                .AllPreviousPaths(
                    new Statement[] { marks[3], marks[2], branchStatement, marks[1] },
                    new Statement[] { marks[3], branchStatement, marks[1] });
        }

        [TestMethod]
        public void Simple_debug_mode_branches_are_followed()
        {
            const string code = @"
public void Method()
{
    Mark(1);
    if (System.Console.Title == """")
    {
        Mark(2);
    }
    Mark(3);
}";

            Method method = GetMethod(code, true);
            StatementFlow flow = new StatementFlow(method);
            AssertEx.That(() => flow.Statements.Count() == 6);

            // Get the statements.
            var marks = GetMarks(flow);
            AssignmentStatement assignmentStatement = flow.Statements.OfType<AssignmentStatement>().Single();
            Branch branchStatement = flow.Statements.OfType<Branch>().Single();
            ReturnNode returnStatement = flow.Statements.OfType<ReturnNode>().Single();

            // Check statement flow.
            new FlowTestHelper(flow)
                .SetStatement(marks[1])
                .PreviousStatements()
                .FollowingStatements(assignmentStatement)

                // Check the assignment statement. The condition is put in a local compiler generated variable.
                .SetStatement(assignmentStatement)
                .PreviousStatements(marks[1])
                .FollowingStatements(branchStatement)

                // Check the branch statement.
                .SetStatement(branchStatement)
                .PreviousStatements(assignmentStatement)
                .FollowingStatements(marks[2], marks[3])

                // Check mark 2.
                .SetStatement(marks[2])
                .PreviousStatements(branchStatement)
                .FollowingStatements(marks[3])

                // Check mark 3.
                .SetStatement(marks[3])
                .PreviousStatements(branchStatement, marks[2])
                .FollowingStatements(returnStatement)

                // Check the return statement.
                .SetStatement(returnStatement)
                .PreviousStatements(marks[3])
                .FollowingStatements();
        }

        [TestMethod]
        public void Release_mode_if_inside_if_is_OK()
        {
            const string code = @"
public void Method()
{
    Mark(1);
    if (Test() == 1)
    {
        if (Test() == 2)
        {
            Mark(2);
        }
        Mark(3);
    }
    Mark(4);
}";

            // Get the statements.
            Method method = GetMethod(code, false);
            StatementFlow flow = new StatementFlow(method);
            AssertEx.That(() => flow.Statements.Count() == 7);

            // Get statements.
            var marks = GetMarks(flow);
            Branch firstBranch = flow.Statements
                .OfType<Branch>()
                .Where(branch => GetValueFromTestExpression(branch) == 1)
                .Single();
            Branch secondBranch = flow.Statements
                .OfType<Branch>()
                .Where(branch => GetValueFromTestExpression(branch) == 2)
                .Single();
            ReturnNode returnStatement = flow.Statements.OfType<ReturnNode>().Single();

            // Check statement flow.
            new FlowTestHelper(flow)
                .SetStatement(marks[1])
                .PreviousStatements()
                .FollowingStatements(firstBranch)

                // Check first branch.
                .SetStatement(firstBranch)
                .PreviousStatements(marks[1])
                .FollowingStatements(secondBranch, marks[4])

                // Check second branch.
                .SetStatement(secondBranch)
                .PreviousStatements(firstBranch)
                .FollowingStatements(marks[2], marks[3])

                // Check mark 2.
                .SetStatement(marks[2])
                .PreviousStatements(secondBranch)
                .FollowingStatements(marks[3])

                // Check mark 3.
                .SetStatement(marks[3])
                .PreviousStatements(marks[2], secondBranch)
                .FollowingStatements(marks[4])

                // Check mark 4.
                .SetStatement(marks[4])
                .PreviousStatements(firstBranch, marks[3])
                .FollowingStatements(returnStatement)

                // Check return statement.
                .SetStatement(returnStatement)
                .PreviousStatements(marks[4])
                .FollowingStatements();
        }

        [TestMethod]
        public void Debug_mode_if_inside_if_is_OK()
        {
            const string code = @"
public void Method()
{
    Mark(1);
    if (Test() == 1)
    {
        if (Test() == 2)
        {
            Mark(2);
        }
        Mark(3);
    }
    Mark(4);
}";

            // Get the statements.
            Method method = GetMethod(code, true);
            StatementFlow flow = new StatementFlow(method);
            AssertEx.That(() => flow.Statements.Count() == 9);

            // Get statements.
            var marks = GetMarks(flow);
            AssignmentStatement[] assignments = flow.Statements
                .OfType<AssignmentStatement>()
                .OrderBy(statement => statement.UniqueKey)
                .ToArray();
            Branch[] branches = flow.Statements
                .OfType<Branch>()
                .OrderBy(branch => branch.UniqueKey)
                .ToArray();
            ReturnNode returnStatement = flow.Statements.OfType<ReturnNode>().Single();

            // Check statement flow.
            new FlowTestHelper(flow)
                .SetStatement(marks[1])
                .PreviousStatements()
                .FollowingStatements(assignments[0])

                // Check first assignment.
                .SetStatement(assignments[0])
                .PreviousStatements(marks[1])
                .FollowingStatements(branches[0])

                // Check first branch.
                .SetStatement(branches[0])
                .PreviousStatements(assignments[0])
                .FollowingStatements(assignments[1], marks[4])

                // Check second assignment.
                .SetStatement(assignments[1])
                .PreviousStatements(branches[0])
                .FollowingStatements(branches[1])

                // Check second branch.
                .SetStatement(branches[1])
                .PreviousStatements(assignments[1])
                .FollowingStatements(marks[2], marks[3])

                // Check mark 2.
                .SetStatement(marks[2])
                .PreviousStatements(branches[1])
                .FollowingStatements(marks[3])

                // Check mark 3.
                .SetStatement(marks[3])
                .PreviousStatements(marks[2], branches[1])
                .FollowingStatements(marks[4])

                // Check mark 4.
                .SetStatement(marks[4])
                .PreviousStatements(branches[0], marks[3])
                .FollowingStatements(returnStatement)

                // Check return statement.
                .SetStatement(returnStatement)
                .PreviousStatements(marks[4])
                .FollowingStatements();
        }

        #region Private Methods
        private static int GetValueFromTestExpression(Branch branch)
        {
            BinaryExpression conditionExpression = branch.Condition as BinaryExpression;
            if (conditionExpression != null)
            {
                Literal literal = conditionExpression.Operand2 as Literal;
                if ((literal != null)
                    && (literal.Type.FullName == "System.Int32"))
                {
                    return (int)literal.Value;
                }
            }

            throw new InvalidOperationException("Unable to find int constant.");
        }

        private static int GetMark(Statement statement)
        {
            ExpressionStatement expressionStatement = statement as ExpressionStatement;
            if (expressionStatement != null)
            {
                MethodCall call = expressionStatement.Expression as MethodCall;
                if (call != null)
                {
                    MemberBinding callee = call.Callee as MemberBinding;
                    if (callee != null)
                    {
                        if ((callee.TargetObject is This)
                            && (callee.BoundMember.FullName == "Class.Mark(System.Int32)"))
                        {
                            return (int)((Literal)call.Operands[0]).Value;
                        }
                    }
                }
            }

            return int.MinValue;
        }

        private static Dictionary<int, Statement> GetMarks(StatementFlow flow)
        {
            return flow.Statements
                .Where(statment => GetMark(statment) != int.MinValue)
                .ToDictionary(statement => GetMark(statement));
        }

        private static Method GetMethod(string methodCode, bool debugMode)
        {
            string code =
@"public class Class
{
" + methodCode + @"

    private void Mark(int mark)
    {
    }

    private int Test()
    {
        return 999;
    }
}";

            var assembly = new CompileHelper()
                .AddSource(code)
                .Generate(debugMode);
            return TypeNodeHelper.GetTypeNode(assembly, "Class")
                .Members
                .OfType<Method>()
                .Where(method => method.Name.Name == "Method")
                .Single();
        }
        #endregion Private Methods
    }

    internal class FlowTestHelper
    {
        private readonly StatementFlow flow;
        private Statement statement;

        internal FlowTestHelper(StatementFlow flow)
        {
            this.flow = flow;
        }

        internal FlowTestHelper SetStatement(Statement statement)
        {
            this.statement = statement;

            return this;
        }

        internal FlowTestHelper AllFollowingStatements(params Statement[] expectedStatements)
        {
            ElementsEqual(
                this.flow.AllFollowingStatements(this.statement),
                expectedStatements);

            return this;
        }

        internal FlowTestHelper AllPreviousStatements(params Statement[] expectedStatements)
        {
            ElementsEqual(
                this.flow.AllPreviousStatements(this.statement),
                expectedStatements);

            return this;
        }

        internal FlowTestHelper FollowingStatements(params Statement[] expectedStatements)
        {
            ElementsEqual(
                this.flow.FollowingStatements(this.statement),
                expectedStatements);

            return this;
        }

        internal FlowTestHelper PreviousStatements(params Statement[] expectedStatements)
        {
            ElementsEqual(
                this.flow.PreviousStatements(this.statement),
                expectedStatements);

            return this;
        }

        internal FlowTestHelper AllFollowingPaths(params IEnumerable<Statement>[] expectedPaths)
        {
            var paths = this.flow.AllFollowingPaths(this.statement).ToArray();
            AssertEx.That(() => expectedPaths.Length == paths.Length);

            for (int i = 0; i < expectedPaths.Length; i++)
            {
                ElementsEqual(paths[i], expectedPaths[i].ToArray());
            }

            return this;
        }

        internal FlowTestHelper AllPreviousPaths(params IEnumerable<Statement>[] expectedPaths)
        {
            var paths = this.flow.AllPreviousPaths(this.statement).ToArray();
            AssertEx.That(() => expectedPaths.Length == paths.Length);

            for (int i = 0; i < expectedPaths.Length; i++)
            {
                ElementsEqual(paths[i], expectedPaths[i].ToArray());
            }

            return this;
        }

        private static void ElementsEqual(IEnumerable<Statement> actualStatements, params Statement[] expectedStatements)
        {
            AssertEx.That(() => actualStatements.Count() == expectedStatements.Length);
            foreach (Statement expectedStatement in expectedStatements)
            {
                AssertEx.That(() => actualStatements.Contains(expectedStatement));
            }
        }
    }
}